Skip to content

Add ZSTD bindings to the node:zlib package.#6007

Open
alistairjevans wants to merge 10 commits intocloudflare:mainfrom
alistairjevans:alistairjevans/zstd-support
Open

Add ZSTD bindings to the node:zlib package.#6007
alistairjevans wants to merge 10 commits intocloudflare:mainfrom
alistairjevans:alistairjevans/zstd-support

Conversation

@alistairjevans
Copy link

Based pretty heavily on the brotli support.

Closes #4013

@alistairjevans alistairjevans requested review from a team as code owners February 3, 2026 11:09
@github-actions
Copy link

github-actions bot commented Feb 3, 2026

All contributors have signed the CLA ✍️ ✅
Posted by the CLA Assistant Lite bot.

@alistairjevans
Copy link
Author

I have read the CLA Document and I hereby sign the CLA

ZstdEncoderContext::~ZstdEncoderContext() {
if (cctx_ != nullptr) {
ZSTD_freeCCtx(cctx_);
cctx_ = nullptr;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It might be worth wrapping this in a custom smart pointer to ensure the cleanup is correct. Non-blocking tho.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added (brotli implementation does already do this)

@jasnell jasnell requested a review from anonrig February 3, 2026 15:25
@jasnell
Copy link
Collaborator

jasnell commented Feb 3, 2026

@anonrig ... given your familiarity with the zlib impl in workerd, it would be good to have your review on this as well.

Comment on lines 828 to 830
constructor(options: ZstdOptions | undefined | null, mode: number) {
ok(mode === CONST_ZSTD_DECODE || mode === CONST_ZSTD_ENCODE);
zstdInitParamsArray.fill(-1);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This diverges from the Node.js implementation and makes it a lot less readable. Ideally the constructor should receive a parameter for initParamsArray just like Node.js

class Zstd extends ZlibBase {
  constructor(opts, mode, initParamsArray, maxParam) {

If we diverge from Node.js, finding bugs & keep the code in sync would be a lot harder then it really is.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated 👍

size = "large",
src = "zstd-nodejs-test.wd-test",
args = ["--experimental"],
data = ["zstd-nodejs-test.js"],
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
data = ["zstd-nodejs-test.js"],
data = ["zlib-zstd-nodejs-test.js"],

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Renamed


wd_test(
size = "large",
src = "zstd-nodejs-test.wd-test",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
src = "zstd-nodejs-test.wd-test",
src = "zlib-zstd-nodejs-test.wd-test",

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Renamed

export const zstdConstantsTest = {
test() {
// Flush directives
assert.strictEqual(typeof zlib.constants.ZSTD_e_continue, 'number');
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You've already tested this in zlib-nodejs-test.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed duplicate

@alistairjevans
Copy link
Author

Thank you for the feedback @anonrig and @jasnell! 🙏 I believe I've addressed all your comments now.

@alistairjevans
Copy link
Author

Merged latest; @anonrig / @jasnell , any chance of a re-review? 🙏

@anonrig
Copy link
Member

anonrig commented Feb 11, 2026

LGTM. @alistairjevans formatter seems to be failing: https://github.com/cloudflare/workerd/actions/runs/21922721360/job/63307147452?pr=6007

@jasnell can you take a look?

@alistairjevans
Copy link
Author

alistairjevans commented Feb 11, 2026

Thanks @anonrig, formatting fix pushed, linter passes locally now.

@jasnell
Copy link
Collaborator

jasnell commented Feb 11, 2026

Overall I think this looks good. What I'm considering... (something I wish I had thought to do on the previous node:zlib additions)... is adding this initially behind an experimental compat flag and having the security team do some quick fuzz testing on it. Anything new that is dealing with bytes being passed around makes me just a bit nervous and a little extra testing doesn't hurt. That's not to say I see anything in here concretely questionable, just thinking about taking the extra precaution. @danlapid what do you think?

@alistairjevans
Copy link
Author

@jasnell, would such a compatibility flag still allow us to opt-in to using the feature in Cloudflare Workers while we wait for the fuzz test to complete?

@jasnell
Copy link
Collaborator

jasnell commented Feb 12, 2026

@alistairjevans ... while the fuzz testing would happening (if we go that route) the compat flag would be marked as "experimental" which means only cloudflare accounts would be able to use it until that annotation is removed. Still need to decide tho if this is the path we'd want to take. Should have a decision soon.

@danlapid
Copy link
Collaborator

danlapid commented Feb 12, 2026

Let's not block - good idea to do as follow up - still want to see this land.
Let's review the code until we're confident this is safe.

Copy link
Collaborator

@jasnell jasnell left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall, LGTM with a couple nits and a question about error handling that might need addressing before this lands. I'll do the sign off once the question is looked at :-) .. great job overall!!

There is a pre-existing issue we need to address with CompressionStream::write's validation of flush values but that's not introduced or made worse with this change (it's relevant for the brotli support also). And, there's missing external memory tracking here that can be done as a follow-up (see, for instance, the CompressionAllocator::AllocForBrotli, CompressionAllocator::FreeForZlib, etc). We'll want to come back to that.

}

kj::Maybe<CompressionError> ZstdDecoderContext::resetStream() {
if (dctx_.get() != nullptr) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be clearing the error_ state? e.g. if work() fails, then resetStream() is called, then work() succeeds, will getError() be returning the right thing?

}

kj::Maybe<CompressionError> ZstdDecoderContext::setParams(int key, int value) {
size_t result = ZSTD_DCtx_setParameter(dctx_.get(), static_cast<ZSTD_dParameter>(key), value);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: should there be a KJ_DASSERT to verify dctx_.get() is valid?

// For decompression, lastResult == 0 means frame is complete
// If we have flush_ == ZSTD_e_end equivalent and there's input left, that's an error
if (flush_ == ZSTD_e_end && input_.pos < input_.size && lastResult == 0) {
// Frame completed but there's still input - could be trailing data
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: what is this if block for if it's empty?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

node:zlib implement zstd

4 participants